<?php
namespace WDB\Validation;
use WDB,
    WDB\Exception;

/**
 * @property-read ColumnValidationError[] $errors
 * @author Richard Ejem <richard(at)ejem.cz>
 * @package WDB
 */
final class ColumnValidator implements iValidator
{
    /**@var array*/
    private $errors;


    //WDB\Event\iEventListener implementation
    /**
     * Validate passed record.
     *
     * @param WDB\Wrapper\Record $record
     * @return bool
     * @throws WDB\Exception\BadArgument
     */
    public function raise(WDB\Wrapper\iRecord $record = NULL)
    {
        if ($record === NULL) throw new Exception\BadArgument("Validator event raise() method needs"
            . " WDB\Wrapper\iRecord as a raise argument");
        $this->errors = array();
        foreach ($record->getTable()->getColumns() as $column)
        {
            $this->validateColumn($column, $record);
        }
        $record->addValidationErrors($this->errors);
        return $this->success();
    }
    //WDB\Event\iEventListener end

    /**
     * Returns true if last validation was successful.
     *
     * @return bool
     */
    public function success()
    {
        return count($this->errors) == 0;
    }

    //TODO create structure for errors
    /**
     * Returns list of validation errors occured
     *
     * @return array
     */
    public function getErrors()
    {
        return $this->errors;
    }
    //iValidator implementation
    public function fetchValidatorData(WDB\Wrapper\iRecord $record, &$data)
    {
        $columns = $record->getTable()->getColumns();
        foreach ($data['columns'] as $key=>&$c)
        {
            if (isset($columns[$key]) && $columns[$key]->getRules() !== NULL)
            {
                $c['ColumnValidator'] = $columns[$key]->getRules()->toArray();
            }
        }
    }
    //iValidator end

    /**
     * If condition is false, adds a message to this->errors based on currently validated column and rule.
     *
     * @param bool $condition
     */
    private function assert(WDB\Wrapper\iColumn $column, $rule, WDB\Wrapper\Field $field, $condition)
    {
        if (!$condition)
        {
            $rules = $column->getRules();
            $err = $rules[$rule]['message'];
            $this->errors[] = array('message'=>$err, 'column'=>$column);
            $field->addValidationError($err);

        }
    }

    /**
     * Validates current column by class private variables (column, field).
     *
     * @throws WDB\Exception\BadArgument
     */
    private function validateColumn(WDB\Wrapper\iColumn $column, WDB\Wrapper\iRecord $record)
    {
        $value = $record[$column->getName()];
        $columns = $record->getTable()->getColumns();
        $c = $column;
        $f = $record->getField($column->getName());
        //column is empty and not required - skip some validation
        $isEmpty = (($value === NULL || $value === '') && empty($column->rules[ColumnRules::REQUIRED]));

        foreach ($column->rules as $id=>$rule)
        {
            $r = $id;
            $special = FALSE;
            if (is_array($value) || is_object($value))
            {
                $special = TRUE;
                if (!in_array($id, array(ColumnRules::REQUIRED, ColumnRules::EQUALTO, ColumnRules::CALLBACK))) continue;
            }

            switch ($id)
            {
                case ColumnRules::REQUIRED:
                    $this->assert($c,$r,$f,($value !== '' && $value !== NULL && $value !== FALSE && $value !== array()));
                    break;
                case ColumnRules::INTEGER:
                    $this->assert($c,$r,$f,preg_match('~[+-]?[0-9]+~', trim($value)));
                    break;
                case ColumnRules::FLOAT:
                    $this->assert($c,$r,$f,is_numeric(trim($value)));
                    break;
                case ColumnRules::MINLENGTH:
                    $this->assert($c,$r,$f,strlen($value) >= $rule['argument']);
                    break;
                case ColumnRules::MAXLENGTH:
                    $this->assert($c,$r,$f,strlen($value) <= $rule['argument']);
                    break;
                case ColumnRules::MINVALUE:
                    $this->assert($c,$r,$f,is_numeric($value) && bccomp($value, $rule['argument']) >= 0);
                    break;
                case ColumnRules::MAXVALUE:
                    $this->assert($c,$r,$f,is_numeric($value) && bccomp($value, $rule['argument']) <= 0);
                    break;
                case ColumnRules::PATTERN:
                    $this->assert($c,$r,$f,$isEmpty || preg_match($rule['argument'], $value));
                    break;
                case ColumnRules::EQUALTO:
                    if (!isset($record[$rule['argument']]))
                    {
                        throw new Exception\BadArgument("field {$rule['argument']} tested for equality with {$column->getName()} was not found");
                    }
                    $this->assert($c,$r,$f,$value == $record[$rule['argument']]);
                    break;
                case ColumnRules::CALLBACK:
                    if (!$rule['argument'] instanceof WDB\Utils\Caller && !is_callable($rule['argument']))
                    {
                        throw new Exception\BadArgument("Validation rule with custom callback need the callback to be an instance of \WDB\Utils\Caller");
                    }
                    if ($rule['argument'] instanceof WDB\Utils\Caller)
                    {
                        $rule['argument']->arguments['value'] = $value;
                        $this->assert($c,$r,$f,$isEmpty || $rule['argument']($value));
                    }
                    else
                    {
                        $this->assert($c,$r,$f,$isEmpty || call_user_func($rule['argument'], $value));
                    }
                    break;
                default:
                    throw new Exception\BadArgument("unknown validation rule #$id");
            }
        }
    }
}
